package com.googlecode.pacswitch.actions;

import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Semaphore;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

import com.googlecode.pacswitch.Log;
import com.googlecode.pacswitch.ObjectFactory;
import com.googlecode.pacswitch.PInteger;
import com.googlecode.pacswitch.PString;
import com.googlecode.pacswitch.PacSwitch;
import com.googlecode.pacswitch.PropertyValue;

/**
 * @author Andrey Pavlenko
 */
@XmlType(name = "Proxies", namespace = ObjectFactory.ANS)
public class Proxies extends Pac implements PropertyValue {
    private final Semaphore _lock = new Semaphore(1);
    private final List<Proxy> _proxies = Collections.checkedList(
            new ArrayList<Proxy>(), Proxy.class);
    private volatile List<Proxy> _sortedProxies = _proxies;
    private volatile long _lastSortTime;
    private PString _testUrl;
    private PInteger _testDelay;

    @Override
    @XmlElement(name = "header", type = Header.class,
        namespace = ObjectFactory.ANS)
    public List<Header> getHeaders() {
        return super.getHeaders();
    }

    public List<Proxy> getProxies() {
        return _proxies;
    }

    @XmlElement(name = "proxy", type = Proxy.class,
        namespace = ObjectFactory.ANS, required = true)
    public List<Proxy> getProxiesAction() {
        return _proxies;
    }

    @XmlElement(name = "proxy", type = Proxy.class,
        namespace = ObjectFactory.PNS, required = true)
    public List<Proxy> getProxiesProperty() {
        return _proxies;
    }

    @XmlAttribute
    public PString getTestUrl() {
        return _testUrl;
    }

    public void setTestUrl(final PString testUrl) {
        _testUrl = testUrl;
    }

    @XmlAttribute
    public PInteger getTestDelay() {
        return _testDelay;
    }

    public void setTestDelay(final PInteger testDelay) {
        _testDelay = testDelay;
    }

    public List<Proxy> getSortedProxies(final PacSwitch ps,
            final AsynchronousSocketChannel clientSocket) {
        final List<Proxy> sortedProxies = _sortedProxies;
        final PString testUrl = getTestUrl();

        if (testUrl != null) {
            final PInteger testDelay = getTestDelay();
            final long delay = (testDelay == null) ? 3600000 : testDelay
                    .resolveInt(ps, clientSocket) * 1000;

            if (((_lastSortTime + delay) < System.currentTimeMillis())
                    && _lock.tryAcquire()) {
                sort(ps, clientSocket);
            }
        }

        return sortedProxies;
    }

    @Override
    public String resolve(final PacSwitch ps,
            final AsynchronousSocketChannel clientSocket) {
        return resolve(ps, clientSocket, getSortedProxies(ps, clientSocket));
    }

    @Override
    protected String resolveBody(final PacSwitch ps,
            final AsynchronousSocketChannel clientSocket) {
        return Proxy.body(resolve(ps, clientSocket));
    }

    private static String resolve(final PacSwitch ps,
            final AsynchronousSocketChannel clientSocket,
            final Collection<Proxy> proxies) {
        final StringBuilder sb = new StringBuilder();

        for (final Iterator<Proxy> it = proxies.iterator(); it.hasNext();) {
            final Proxy p = it.next();
            sb.append("PROXY ");
            sb.append(p.resolve(ps, clientSocket));

            if (it.hasNext()) {
                sb.append("; ");
            }
        }

        return sb.toString();
    }

    private void sort(final PacSwitch ps,
            final AsynchronousSocketChannel clientSocket) {
        new Thread() {
            @Override
            public void run() {
                try {
                    final Log log = ps.getConfig().getLog();
                    final List<Proxy> proxies = getProxies();
                    final List<ProxyWrapper> wrappers = new ArrayList<>(
                            proxies.size());
                    final List<Proxy> sortedProxies = new ArrayList<>(
                            proxies.size());
                    URL url;

                    try {
                        url = new URL(_testUrl.resolve(ps, clientSocket));
                    } catch (final MalformedURLException ex) {
                        log.error("Error occurred", ex);
                        return;
                    }

                    if (log.isDebugEnabled()) {
                        log.debug("Sorting proxies with url " + getTestUrl());
                    }

                    for (final Proxy p : proxies) {
                        long latency = Long.MAX_VALUE;

                        if (log.isDebugEnabled()) {
                            log.debug("Testing proxy: " + p.getHost() + ':'
                                    + p.getPort());
                        }

                        try {
                            latency = p.test(ps, url);

                            if (log.isDebugEnabled()) {
                                log.debug("Proxy "
                                        + p.getHost().resolve(ps, clientSocket)
                                        + ':'
                                        + p.getPort().resolveInt(ps,
                                                clientSocket) + " time: "
                                        + latency + " ms");
                            }
                        } catch (final Throwable ex) {
                            if (log.isErrorEnabled()) {
                                log.error(
                                        "Failed to test proxy: "
                                                + p.getHost().resolve(ps,
                                                        clientSocket)
                                                + ':'
                                                + p.getPort().resolveInt(ps,
                                                        clientSocket), ex);
                            }
                        }

                        wrappers.add(new ProxyWrapper(p, latency));
                    }

                    Collections.sort(wrappers);

                    for (final ProxyWrapper w : wrappers) {
                        sortedProxies.add(w._proxy);
                    }

                    if (log.isDebugEnabled()) {
                        log.debug("Sorted proxies: "
                                + resolve(ps, clientSocket, sortedProxies));
                    }

                    _sortedProxies = sortedProxies;
                    _lastSortTime = System.currentTimeMillis();
                } finally {
                    _lock.release();
                }
            }
        }.start();
    }

    private static final class ProxyWrapper implements Comparable<ProxyWrapper> {
        final Proxy _proxy;
        final long _latency;

        public ProxyWrapper(final Proxy proxy, final long latency) {
            _proxy = proxy;
            _latency = latency;
        }

        @Override
        public int compareTo(final ProxyWrapper p) {
            return _latency < p._latency ? -1 : _latency > p._latency ? 1 : 0;
        }

        @Override
        public int hashCode() {
            return _proxy.hashCode();
        }

        @Override
        public boolean equals(final Object obj) {
            return (obj == this) ? true : (obj instanceof ProxyWrapper)
                    ? ((ProxyWrapper) obj)._proxy.equals(_proxy) : false;
        }
    }
}
